/**
 *    Copyright 2009-2015 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.io;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

/**
 * A default implementation of {@link VFS} that works for most application
 * servers. 默认的VFS，缺省的VFS资源加载实现,提供了读取jar包的方法
 * 
 * @author Ben Gunter
 */
public class DefaultVFS extends VFS {
	private static final Log log = LogFactory.getLog(ResolverUtil.class);

	/** The magic header that indicates a JAR (ZIP) file. */
	private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };

	@Override
	public boolean isValid() {
		return true;
	}

	@Override
	public List<String> list(URL url, String path) throws IOException {
		InputStream is = null;
		try {
			List<String> resources = new ArrayList<String>();

			// First, try to find the URL of a JAR file containing the requested
			// resource. If a JAR file is found, then we'll list child resources
			// by reading the
			// JAR.(首先，尝试找到包含请求的资源的JAR文件的URL。如果找到一个JAR文件，那么我们将通过读取jar来列出子资源。)
			URL jarUrl = findJarForResource(url);
			if (jarUrl != null) {
				is = jarUrl.openStream();
				log.debug("Listing " + url);
				// 用JDK自带的JarInputStream来读取jar包
				resources = listResources(new JarInputStream(is), path);
			} else {
				List<String> children = new ArrayList<String>();
				try {
					if (isJar(url)) {
						// Some versions of JBoss VFS might give a JAR stream
						// even if the resource referenced by the URL isn't
						// actually a JAR
						// (JBaseVFS的一些版本可能会给出JAR流，即使URL引用的资源实际上不是jar)
						is = url.openStream();
						JarInputStream jarInput = new JarInputStream(is);
						log.debug("Listing " + url);
						//for(JarEntry entry=jarInput.getNextJarEntry(); entry != null; entry=jarInput.getNextJarEntry())
						for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
							log.debug("Jar entry: " + entry.getName());
							children.add(entry.getName());
						}
						jarInput.close();
					} else {
						/*
						 * Some servlet containers allow reading from directory
						 * resources like a text file, listing the child
						 * resources one per line. However, there is no way to
						 * differentiate between directory and file resources
						 * just by reading them. To work around that, as each
						 * line is read, try to look it up via the class loader
						 * as a child of the current resource. If any line fails
						 * then we assume the current resource is not a
						 * directory.(一些servlet容器允许从目录资源读取文本文件，列出每行一个子资源。然而，
						 * 只有通过读取它们，才能区分目录和文件资源。为了解决这一问题，在读取每一行时，
						 * 尝试通过类加载器将其作为当前资源的子对象进行查找。如果任何行失败，那么我们假设当前资源不是一个目录。)
						 */
						is = url.openStream();
						BufferedReader reader = new BufferedReader(new InputStreamReader(is));
						List<String> lines = new ArrayList<String>();
						for (String line; (line = reader.readLine()) != null;) {
							log.debug("Reader entry: " + line);
							lines.add(line);
							if (getResources(path + "/" + line).isEmpty()) {
								lines.clear();
								break;
							}
						}

						if (!lines.isEmpty()) {
							log.debug("Listing " + url);
							children.addAll(lines);
						}
					}
				} catch (FileNotFoundException e) {
					/*
					 * For file URLs the openStream() call might fail, depending
					 * on the servlet container, because directories can't be
					 * opened for reading. If that happens, then list the
					 * directory directly
					 * instead.(对于文件URL，OpenServer（）调用可能会失败，这取决于servlet容器，
					 * 因为目录不能打开以供阅读。如果发生这种情况，则直接列出目录。)
					 */
					if ("file".equals(url.getProtocol())) {
						File file = new File(url.getFile());
						log.debug("Listing directory " + file.getAbsolutePath());
						if (file.isDirectory()) {
							log.debug("Listing " + url);
							children = Arrays.asList(file.list());
						}
					} else {
						// No idea where the exception came from so rethrow it
						throw e;
					}
				}

				// The URL prefix to use when recursively listing child
				// resources(递归列出子资源时使用的URL前缀)
				String prefix = url.toExternalForm();
				if (!prefix.endsWith("/")) {
					prefix = prefix + "/";
				}

				// Iterate over immediate children, adding files and recursing
				// into directories (迭代即时文件，添加文件并递归到目录中)
				for (String child : children) {
					String resourcePath = path + "/" + child;
					resources.add(resourcePath);
					URL childUrl = new URL(prefix + child);
					resources.addAll(list(childUrl, resourcePath));
				}
			}

			return resources;
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (Exception e) {
					// Ignore
				}
			}
		}
	}

	/**
	 * List the names of the entries in the given {@link JarInputStream} that
	 * begin with the specified {@code path}. Entries will match with or without
	 * a leading slash.(列出从指定的{@代码路径}开始的给定中的条目的名称。条目将与有或没有领先斜杠匹配)
	 * 
	 * @param jar
	 *            The JAR input stream
	 * @param path
	 *            The leading path to match
	 * @return The names of all the matching entries
	 * @throws IOException
	 *             If I/O errors occur
	 */
	protected List<String> listResources(JarInputStream jar, String path) throws IOException {
		// Include the leading and trailing slash when matching names
		if (!path.startsWith("/")) {
			path = "/" + path;
		}
		if (!path.endsWith("/")) {
			path = path + "/";
		}

		// Iterate over the entries and collect those that begin with the
		// requested path(迭代条目并收集以请求的路径开始的条目。)
		List<String> resources = new ArrayList<String>();
		for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
			if (!entry.isDirectory()) {
				// Add leading slash if it's missing(如果缺少，添加前导斜杠)
				String name = entry.getName();
				if (!name.startsWith("/")) {
					name = "/" + name;
				}

				// Check file name(检查文件名)
				if (name.startsWith(path)) {
					log.debug("Found resource: " + name);
					// Trim leading slash(修剪)
					resources.add(name.substring(1));
				}
			}
		}
		return resources;
	}

	/**
	 * Attempts to deconstruct the given URL to find a JAR file containing the
	 * resource referenced by the URL. That is, assuming the URL references a
	 * JAR entry, this method will return a URL that references the JAR file
	 * containing the entry. If the JAR cannot be located, then this method
	 * returns null.(试图解压给定的URL以找到包含URL引用的资源的JAR文件。也就是说，假设URL引用JAR条目，
	 * 该方法将返回引用包含该条目的JAR文件的URL。如果jar无法定位，则此方法返回null。)
	 * 
	 * @param url
	 *            The URL of the JAR entry.
	 * @return The URL of the JAR file, if one is found. Null if not.
	 * @throws MalformedURLException
	 */
	protected URL findJarForResource(URL url) throws MalformedURLException {
		log.debug("Find JAR URL: " + url);

		// If the file part of the URL is itself a URL, then that URL probably
		// points to the JAR(如果URL的文件部分本身是URL，那么该URL可能指向jar。)
		try {
			for (;;) {
				url = new URL(url.getFile());
				log.debug("Inner URL: " + url);
			}
		} catch (MalformedURLException e) {
			// This will happen at some point and serves as a break in the loop
		}

		// Look for the .jar extension and chop off everything after that(寻找.jar扩展，然后删除所有的东西)
		StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
		int index = jarUrl.lastIndexOf(".jar");
		if (index >= 0) {
			jarUrl.setLength(index + 4);
			log.debug("Extracted JAR URL: " + jarUrl);
		} else {
			log.debug("Not a JAR: " + jarUrl);
			return null;
		}

		// Try to open and test it(尝试打开和测试它)
		try {
			URL testUrl = new URL(jarUrl.toString());
			if (isJar(testUrl)) {
				return testUrl;
			} else {
				// WebLogic fix: check if the URL's file exists in the filesystem.(检查URL的文件是否存在于文件系统中。)
				log.debug("Not a JAR: " + jarUrl);
				jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
				File file = new File(jarUrl.toString());

				// File name might be URL-encoded(文件名可能是URL编码)
				if (!file.exists()) {
					try {
						file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
					} catch (UnsupportedEncodingException e) {
						throw new RuntimeException("Unsupported encoding?  UTF-8?  That's unpossible.");
					}
				}

				if (file.exists()) {
					log.debug("Trying real file: " + file.getAbsolutePath());
					testUrl = file.toURI().toURL();
					if (isJar(testUrl)) {
						return testUrl;
					}
				}
			}
		} catch (MalformedURLException e) {
			log.warn("Invalid JAR URL: " + jarUrl);
		}

		log.debug("Not a JAR: " + jarUrl);
		return null;
	}

	/**
	 * Converts a Java package name to a path that can be looked up with a call
	 * to {@link ClassLoader#getResources(String)}.(将一个java包名转化为包路径)
	 * 
	 * @param packageName
	 *            The Java package name to convert to a path
	 */
	protected String getPackagePath(String packageName) {
		return packageName == null ? null : packageName.replace('.', '/');
	}

	/**
	 * Returns true if the resource located at the given URL is a JAR file.(判断是否为Jar)
	 * 
	 * @param url
	 *            The URL of the resource to test.
	 */
	protected boolean isJar(URL url) {
		return isJar(url, new byte[JAR_MAGIC.length]);
	}

	/**
	 * Returns true if the resource located at the given URL is a JAR file.(判断是否为Jar)
	 * 
	 * @param url
	 *            The URL of the resource to test.
	 * @param buffer
	 *            A buffer into which the first few bytes of the resource are
	 *            read. The buffer must be at least the size of
	 *            {@link #JAR_MAGIC}. (The same buffer may be reused for
	 *            multiple calls as an optimization.)
	 */
	protected boolean isJar(URL url, byte[] buffer) {
		InputStream is = null;
		try {
			is = url.openStream();
			is.read(buffer, 0, JAR_MAGIC.length);
			if (Arrays.equals(buffer, JAR_MAGIC)) {
				log.debug("Found JAR: " + url);
				return true;
			}
		} catch (Exception e) {
			// Failure to read the stream means this is not a JAR
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch (Exception e) {
					// Ignore
				}
			}
		}

		return false;
	}
}
